import os
import json
import logging
import subprocess
import random
import socket
from typing import Dict, List, Any, Optional
from jinja2 import Environment, FileSystemLoader, select_autoescape

from config import settings

logger = logging.getLogger("mobox-server.docker-service")

class DockerService:
    """Docker服务类，用于管理Docker容器"""
    
    def __init__(self):
        """初始化Docker服务"""
        # 设置Jinja2模板环境
        self.jinja_env = Environment(
            loader=FileSystemLoader("templates"),
            autoescape=select_autoescape(['yml', 'yaml'])
        )
        
        # 当前正在使用的端口映射
        self.active_ports = set()
        self._load_active_ports()
    
    def _load_active_ports(self):
        """加载当前正在使用的端口"""
        try:
            # 使用docker-compose ps获取当前运行的容器
            result = subprocess.run(
                ["docker", "ps", "--format", "{{.Ports}}"],
                capture_output=True,
                text=True,
                check=True
            )
            
            ports = set()
            for line in result.stdout.splitlines():
                if "->" in line:
                    # 解析端口映射，格式类似 0.0.0.0:8080->80/tcp
                    port_mappings = line.split(",")
                    for mapping in port_mappings:
                        if "->" in mapping:
                            host_port = mapping.split("->")[0].split(":")[-1]
                            try:
                                ports.add(int(host_port))
                            except ValueError:
                                continue
            
            self.active_ports = ports
            logger.info(f"已加载活跃端口: {len(self.active_ports)} 个")
        except Exception as e:
            logger.error(f"加载活跃端口时出错: {str(e)}", exc_info=True)
    
    def _get_free_port(self) -> int:
        """获取一个空闲的端口"""
        port_range = range(settings.PORT_RANGE_START, settings.PORT_RANGE_END + 1)
        available_ports = list(set(port_range) - self.active_ports)
        
        if not available_ports:
            raise ValueError("没有可用的端口")
        
        # 随机选择一个可用端口
        port = random.choice(available_ports)
        
        # 检查端口是否真的可用
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            if s.connect_ex(('localhost', port)) == 0:
                # 端口已被占用，递归查找另一个端口
                return self._get_free_port()
        
        self.active_ports.add(port)
        return port
    
    def generate_docker_compose(self, 
                               user_id: str, 
                               experiment_id: str, 
                               course_type: str, 
                               port: int,
                               user_dir: str,
                               course_files_path: str) -> str:
        """
        生成docker-compose文件
        
        Args:
            user_id: 用户ID
            experiment_id: 实验ID
            course_type: 课程类型
            port: 端口号
            user_dir: 用户目录
            course_files_path: 课程文件路径
            
        Returns:
            str: 生成的docker-compose文件路径
        """
        template = self.jinja_env.get_template("docker-compose.yml.j2")
        service_name = f"{user_id}_{course_type}_{experiment_id}"
        
        # 构建模板上下文
        context = {
            "service_name": service_name,
            "user_id": user_id,
            "experiment_id": experiment_id,
            "course_type": course_type,
            "port": port,
            "user_dir": user_dir,
            "course_files_path": course_files_path,
            "record_progress": course_type.lower() == "club"  # 如果是社团课，需要记录进度
        }
        
        # 渲染模板
        docker_compose_content = template.render(**context)
        
        # 保存到文件
        output_dir = os.path.join(user_dir, "docker_compose")
        os.makedirs(output_dir, exist_ok=True)
        output_file = os.path.join(output_dir, f"{service_name}.yml")
        
        with open(output_file, 'w') as f:
            f.write(docker_compose_content)
        
        logger.info(f"已生成docker-compose文件: {output_file}")
        return output_file
    
    def start_experiment_service(self, 
                                user_id: str, 
                                experiment_id: str, 
                                course_type: str,
                                user_dir: str,
                                course_files_path: str) -> str:
        """
        启动实验服务
        
        Args:
            user_id: 用户ID
            experiment_id: 实验ID
            course_type: 课程类型
            user_dir: 用户目录路径
            course_files_path: 课程文件路径
            
        Returns:
            str: 服务访问URL
        """
        # 分配端口
        port = self._get_free_port()
        
        # 生成docker-compose文件
        docker_compose_file = self.generate_docker_compose(
            user_id=user_id,
            experiment_id=experiment_id,
            course_type=course_type,
            port=port,
            user_dir=user_dir,
            course_files_path=course_files_path
        )
        
        # 获取服务名称
        service_name = f"{user_id}_{course_type}_{experiment_id}"
        
        try:
            # 使用docker-compose启动服务
            logger.info(f"开始启动服务: {service_name}")
            result = subprocess.run(
                ["docker-compose", "-f", docker_compose_file, "-p", service_name, "up", "-d"],
                capture_output=True,
                text=True,
                check=True
            )
            
            logger.info(f"服务启动成功: {service_name}")
            logger.debug(f"Docker输出: {result.stdout}")
            
            # 构建服务访问URL
            service_url = f"http://localhost:{port}/experiment?id={experiment_id}"
            
            # 如果是在生产环境，可能需要替换为实际的主机名或IP
            # TODO: 根据实际环境配置URL
            
            return service_url
            
        except subprocess.CalledProcessError as e:
            logger.error(f"启动服务失败: {str(e)}", exc_info=True)
            logger.error(f"Docker错误输出: {e.stderr}")
            raise ValueError(f"启动Docker服务失败: {e.stderr}")
    
    def stop_experiment_service(self, user_id: str, experiment_id: str, course_type: str) -> bool:
        """
        停止实验服务
        
        Args:
            user_id: 用户ID
            experiment_id: 实验ID
            course_type: 课程类型
            
        Returns:
            bool: 操作是否成功
        """
        service_name = f"{user_id}_{course_type}_{experiment_id}"
        user_dir = os.path.join(settings.USER_DATA_PATH, user_id)
        docker_compose_file = os.path.join(user_dir, "docker_compose", f"{service_name}.yml")
        
        if not os.path.exists(docker_compose_file):
            logger.warning(f"找不到Docker Compose文件: {docker_compose_file}")
            return False
        
        try:
            # 使用docker-compose停止服务
            logger.info(f"开始停止服务: {service_name}")
            result = subprocess.run(
                ["docker-compose", "-f", docker_compose_file, "-p", service_name, "down"],
                capture_output=True,
                text=True,
                check=True
            )
            
            logger.info(f"服务停止成功: {service_name}")
            logger.debug(f"Docker输出: {result.stdout}")
            
            # 更新活跃端口列表
            self._load_active_ports()
            
            return True
            
        except subprocess.CalledProcessError as e:
            logger.error(f"停止服务失败: {str(e)}", exc_info=True)
            logger.error(f"Docker错误输出: {e.stderr}")
            return False
    
    def list_user_experiments(self, user_id: str) -> List[Dict[str, Any]]:
        """
        列出用户的实验
        
        Args:
            user_id: 用户ID
            
        Returns:
            List[Dict[str, Any]]: 实验列表
        """
        user_dir = os.path.join(settings.USER_DATA_PATH, user_id)
        docker_compose_dir = os.path.join(user_dir, "docker_compose")
        
        if not os.path.exists(docker_compose_dir):
            return []
        
        experiments = []
        
        for filename in os.listdir(docker_compose_dir):
            if filename.endswith(".yml") and filename.startswith(f"{user_id}_"):
                parts = filename[:-4].split("_")
                if len(parts) >= 3:
                    # 解析文件名获取course_type和experiment_id
                    course_type = parts[1]
                    experiment_id = parts[2]
                    
                    # 检查服务是否正在运行
                    service_name = f"{user_id}_{course_type}_{experiment_id}"
                    is_running = self._check_service_running(service_name)
                    
                    experiments.append({
                        "user_id": user_id,
                        "course_type": course_type,
                        "experiment_id": experiment_id,
                        "status": "running" if is_running else "stopped"
                    })
        
        return experiments
    
    def _check_service_running(self, service_name: str) -> bool:
        """
        检查服务是否在运行
        
        Args:
            service_name: 服务名称
            
        Returns:
            bool: 服务是否在运行
        """
        try:
            result = subprocess.run(
                ["docker-compose", "-p", service_name, "ps", "-q"],
                capture_output=True,
                text=True,
                check=True
            )
            
            return bool(result.stdout.strip())
        except Exception as e:
            logger.error(f"检查服务状态时出错: {str(e)}", exc_info=True)
            return False 